"""
Fish.audio 音频抓取脚本
抓取 https://fish.audio/zh-CN/app/discovery/ 页面的所有音频文件
"""

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.keys import Keys
from time import sleep
import requests
import os
import json
import logging
from datetime import datetime

# 配置日志
log_file = f"fishAudio_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler(log_file, encoding='utf-8'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)


class FishAudioScraper:
    def __init__(self, download_dir, max_page=10):
        """
        初始化抓取器
        :param download_dir: 音频下载目录
        :param max_page: 最大抓取页数
        """
        self.download_dir = download_dir
        self.max_page = max_page
        self.driver = None
        self.audio_list = []
        self.audio_count = 0
        self.processed_urls = set()  # 记录已处理的音频URL,避免重复
        self.last_element_count = 0  # 记录上次处理的元素数量

        # 确保下载目录存在
        if not os.path.exists(download_dir):
            os.makedirs(download_dir)
            logger.info(f"创建下载目录: {download_dir}")

    def init_driver(self):
        """初始化 Chrome 浏览器"""
        logger.info("正在初始化 Chrome 浏览器...")

        chrome_options = Options()
        chrome_options.add_argument("--start-minimized")  # 最小化窗口
        chrome_options.add_argument("--disable-blink-features=AutomationControlled")
        chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
        chrome_options.add_experimental_option('useAutomationExtension', False)

        # 设置代理
        chrome_options.add_argument('--proxy-server=http://127.0.0.1:7890')

        service = Service('D:\\chromedriver-win64\\chromedriver.exe')
        self.driver = webdriver.Chrome(service=service, options=chrome_options)
        self.driver.set_window_size(1600, 1000)

        logger.info("Chrome 浏览器初始化完成")

    def start_fetch(self):
        """开始抓取"""
        try:
            self.init_driver()

            url = "https://fish.audio/zh-CN/app/discovery/"
            logger.info(f"开始访问页面: {url}")
            self.driver.get(url)

            # 等待页面加载
            sleep(5)
            logger.info("页面加载完成，开始分析页面结构...")

            # 检查是否需要登录
            self.check_login_status()

            # 开始抓取音频
            page = 1
            while page <= self.max_page:
                logger.info(f"\n{'='*50}")
                logger.info(f"开始抓取第 {page} 页")
                logger.info(f"{'='*50}")

                success = self.fetch_page_audios(page)

                if not success:
                    logger.warning("抓取失败或已到达最后一页")
                    break

                # 滚动到底部加载更多
                if page < self.max_page:
                    logger.info("滚动页面加载更多内容...")
                    self.scroll_to_bottom()
                    sleep(3)

                page += 1

            logger.info(f"\n{'='*50}")
            logger.info(f"抓取完成！共抓取 {self.audio_count} 个音频文件")
            logger.info(f"保存目录: {self.download_dir}")
            logger.info(f"{'='*50}")

        except Exception as e:
            logger.error(f"抓取过程出错: {str(e)}", exc_info=True)
        finally:
            if self.driver:
                logger.info("关闭浏览器...")
                sleep(3)
                self.driver.quit()

    def check_login_status(self):
        """检查登录状态"""
        try:
            # 尝试查找登录按钮
            login_buttons = self.driver.find_elements(By.XPATH, "//*[contains(text(), '登录') or contains(text(), 'Login')]")

            if login_buttons:
                logger.warning("检测到未登录状态，某些功能可能受限")
                logger.info("如需登录，请手动登录后按回车继续...")
                # input("按回车继续...")
            else:
                logger.info("已登录或无需登录")
        except Exception as e:
            logger.info(f"登录状态检查: {str(e)}")

    def fetch_page_audios(self, page_num):
        """
        抓取当前页面的音频
        :param page_num: 页码
        :return: 是否成功
        """
        try:
            # 等待页面内容加载
            sleep(2)

            # 打印当前页面的 HTML 结构（调试用）
            logger.info("开始分析页面元素...")

            # 尝试多种可能的选择器来查找音频卡片
            selectors = [
                "div[class*='card']",
                "div[class*='audio']",
                "div[class*='voice']",
                "div[class*='item']",
                "a[href*='/model/']",
                "div[role='button']",
            ]

            audio_elements = []
            for selector in selectors:
                try:
                    elements = self.driver.find_elements(By.CSS_SELECTOR, selector)
                    if elements and len(elements) > 5:  # 如果找到多个元素
                        logger.info(f"使用选择器 '{selector}' 找到 {len(elements)} 个元素")
                        audio_elements = elements
                        break
                except Exception as e:
                    continue

            if not audio_elements:
                logger.warning("未找到音频元素，尝试打印页面源码的前2000字符...")
                page_source = self.driver.page_source[:2000]
                logger.debug(f"页面源码片段: {page_source}")

                # 保存完整的页面源码供调试
                debug_file = f"page_debug_{page_num}.html"
                with open(debug_file, 'w', encoding='utf-8') as f:
                    f.write(self.driver.page_source)
                logger.info(f"完整页面源码已保存到: {debug_file}")

                return False

            total_elements = len(audio_elements)
            logger.info(f"第 {page_num} 页找到 {total_elements} 个潜在的音频元素")

            # **关键改进: 只处理新加载的元素,跳过之前已处理的**
            # 从上次处理的位置继续
            start_index = self.last_element_count
            new_elements = audio_elements[start_index:]

            logger.info(f"上次已处理 {self.last_element_count} 个元素")
            logger.info(f"本次需要处理 {len(new_elements)} 个新元素 (从索引 {start_index} 开始)")

            if len(new_elements) == 0:
                logger.warning("没有新元素需要处理,可能已经到达末尾")
                return False

            # 更新已处理元素计数
            self.last_element_count = total_elements

            # 遍历新的音频元素(限制每次最多处理20个)
            success_count = 0
            process_limit = min(20, len(new_elements))

            for idx, element in enumerate(new_elements[:process_limit]):
                try:
                    actual_index = start_index + idx
                    logger.info(f"\n处理第 {idx + 1}/{process_limit} 个新元素 (总索引: {actual_index})...")

                    # 尝试提取音频信息
                    audio_info = self.extract_audio_info(element, actual_index)

                    if audio_info:
                        # 检查URL是否已处理过
                        if audio_info['url'] in self.processed_urls:
                            logger.info(f"音频 URL 已处理过,跳过: {audio_info['url']}")
                            continue

                        # 下载音频
                        download_success = self.download_audio(audio_info)
                        if download_success:
                            self.processed_urls.add(audio_info['url'])
                            success_count += 1

                except Exception as e:
                    logger.error(f"处理元素 {idx + 1} 时出错: {str(e)}")
                    continue

            logger.info(f"第 {page_num} 页成功处理 {success_count} 个新音频")
            return success_count > 0

        except Exception as e:
            logger.error(f"抓取第 {page_num} 页时出错: {str(e)}", exc_info=True)
            return False

    def extract_audio_info(self, element, idx):
        """
        从元素中提取音频信息
        :param element: 网页元素
        :param idx: 索引
        :return: 音频信息字典
        """
        try:
            # 打印元素的 HTML 内容（调试用）
            element_html = element.get_attribute('outerHTML')[:500]
            logger.debug(f"元素 HTML 片段: {element_html}")

            # **重要: 先提取标题，再点击元素**
            # 因为点击后可能会打开弹窗,导致提取到错误的文本
            title = f"audio_{self.audio_count + 1}"
            try:
                # 尝试多种方式获取标题
                title_selectors = [
                    "[class*='title']",  # 优先查找title类
                    "[class*='name']",   # 其次查找name类
                    "h1", "h2", "h3", "h4",
                    "p[class*='text']",  # 包含text类的p标签
                    "span[class*='text']",  # 包含text类的span标签
                    "div[class*='name']",  # 包含name类的div标签
                ]

                for selector in title_selectors:
                    try:
                        title_elements = element.find_elements(By.CSS_SELECTOR, selector)
                        # 遍历找到的所有元素,选择第一个有效的文本
                        for title_element in title_elements:
                            text = title_element.text.strip()
                            # 过滤掉太长或太短的文本,以及数字
                            if text and 2 < len(text) < 50 and not text.isdigit():
                                title = text
                                logger.info(f"找到标题: {title} (使用选择器: {selector})")
                                break
                        if title != f"audio_{self.audio_count + 1}":
                            break  # 如果找到了有效标题,跳出循环
                    except:
                        continue

                if title == f"audio_{self.audio_count + 1}":
                    # 如果还是默认值,尝试获取卡片内所有文本
                    try:
                        all_text = element.text.strip()
                        lines = [line.strip() for line in all_text.split('\n') if line.strip()]
                        # 选择第一个看起来像标题的行(不是纯数字,长度合适)
                        for line in lines:
                            if line and 2 < len(line) < 50 and not line.isdigit():
                                title = line
                                logger.info(f"从卡片文本提取标题: {title}")
                                break
                    except:
                        pass

            except Exception as e:
                logger.warning(f"提取标题时出错: {str(e)}")

            # 尝试获取音频链接
            audio_url = None

            # 方法1: 查找 audio 标签
            try:
                audio_tag = element.find_element(By.TAG_NAME, "audio")
                audio_url = audio_tag.get_attribute("src")
                logger.info(f"找到 audio 标签，URL: {audio_url}")
            except:
                pass

            # 方法2: 查找包含音频链接的属性
            if not audio_url:
                try:
                    # 查找所有可能包含音频链接的属性
                    for attr in ['data-src', 'data-audio', 'data-url', 'href']:
                        url = element.get_attribute(attr)
                        if url and ('.mp3' in url or '.wav' in url or '.m4a' in url):
                            audio_url = url
                            logger.info(f"从属性 {attr} 找到音频 URL: {audio_url}")
                            break
                except:
                    pass

            # 方法3: 点击元素触发播放，从网络请求中获取
            if not audio_url:
                logger.info("尝试点击元素获取音频 URL...")
                try:
                    # 滚动到元素可见
                    self.driver.execute_script("arguments[0].scrollIntoView(true);", element)
                    sleep(0.5)

                    # 点击元素
                    element.click()
                    sleep(2)

                    # 查找播放器中的音频源
                    try:
                        audio_player = self.driver.find_element(By.TAG_NAME, "audio")
                        audio_url = audio_player.get_attribute("src")
                        logger.info(f"从播放器找到音频 URL: {audio_url}")
                    except:
                        pass

                except Exception as e:
                    logger.warning(f"点击元素失败: {str(e)}")

            if not audio_url:
                logger.warning(f"元素 {idx} 未找到音频 URL")
                return None

            logger.info(f"最终音频标题: {title}")

            return {
                'url': audio_url,
                'title': title,
                'index': self.audio_count
            }

        except Exception as e:
            logger.error(f"提取音频信息失败: {str(e)}", exc_info=True)
            return None

    def download_audio(self, audio_info):
        """
        下载音频文件
        :param audio_info: 音频信息
        :return: 是否下载成功
        """
        try:
            url = audio_info['url']
            title = audio_info['title']

            # 清理文件名中的非法字符
            safe_title = "".join(c for c in title if c.isalnum() or c in (' ', '-', '_')).strip()
            if not safe_title:
                safe_title = f"audio_{audio_info['index']}"

            # 确定文件扩展名
            ext = '.mp3'
            if '.wav' in url.lower():
                ext = '.wav'
            elif '.m4a' in url.lower():
                ext = '.m4a'
            elif '.ogg' in url.lower():
                ext = '.ogg'

            filename = f"{safe_title}{ext}"
            filepath = os.path.join(self.download_dir, filename)

            # 如果文件已存在，跳过但返回True(视为成功)
            if os.path.exists(filepath):
                logger.info(f"文件已存在，跳过: {filename}")
                return True

            logger.info(f"开始下载: {filename}")
            logger.info(f"URL: {url}")

            # 下载文件
            headers = {
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
            }

            proxies = {
                'http': 'http://127.0.0.1:7890',
                'https': 'http://127.0.0.1:7890'
            }

            response = requests.get(url, headers=headers, proxies=proxies, timeout=30)

            if response.status_code == 200:
                with open(filepath, 'wb') as f:
                    f.write(response.content)

                file_size = os.path.getsize(filepath)
                logger.info(f"下载成功: {filename} ({file_size} bytes)")

                self.audio_count += 1
                self.audio_list.append(audio_info)
                return True
            else:
                logger.error(f"下载失败，状态码: {response.status_code}")
                return False

        except Exception as e:
            logger.error(f"下载音频失败: {str(e)}", exc_info=True)
            return False

    def scroll_to_bottom(self):
        """滚动到页面底部"""
        try:
            # 多次滚动以触发懒加载
            for i in range(5):
                self.driver.find_element(By.TAG_NAME, 'body').send_keys(Keys.PAGE_DOWN)
                sleep(0.5)

            # 滚动到最底部
            self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
            sleep(1)

        except Exception as e:
            logger.warning(f"滚动页面失败: {str(e)}")


def main():
    """主函数"""
    # 配置参数
    download_dir = r"H:\python-project\音色\fish"
    max_page = 20  # 最大抓取页数

    logger.info("="*60)
    logger.info("Fish.audio 音频抓取脚本")
    logger.info("="*60)
    logger.info(f"下载目录: {download_dir}")
    logger.info(f"最大页数: {max_page}")
    logger.info(f"日志文件: {log_file}")
    logger.info("="*60)

    # 创建抓取器并开始抓取
    scraper = FishAudioScraper(download_dir, max_page)
    scraper.start_fetch()


if __name__ == '__main__':
    main()
